/*******************************************************************************
 * Copyright (c) 2012-2017 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.api.factory.server.jpa;

import com.google.inject.persist.Transactional;

import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.core.notification.EventService;
import org.eclipse.che.api.factory.server.model.impl.FactoryImpl;
import org.eclipse.che.api.factory.server.spi.FactoryDao;
import org.eclipse.che.api.user.server.event.BeforeUserRemovedEvent;
import org.eclipse.che.api.workspace.server.model.impl.ProjectConfigImpl;
import org.eclipse.che.commons.lang.Pair;
import org.eclipse.che.core.db.cascade.CascadeEventSubscriber;
import org.eclipse.che.core.db.jpa.DuplicateKeyException;
import org.eclipse.che.core.db.jpa.IntegrityConstraintViolationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import java.util.stream.Collectors;

import static java.lang.String.format;
import static java.util.Collections.singletonList;
import static java.util.Objects.requireNonNull;

/**
 * @author Anton Korneta
 */
@Singleton
public class JpaFactoryDao implements FactoryDao {
    private static final Logger LOG = LoggerFactory.getLogger(JpaFactoryDao.class);

    @Inject
    private Provider<EntityManager> managerProvider;

    @Override
    public FactoryImpl create(FactoryImpl factory) throws ConflictException, ServerException {
        requireNonNull(factory);
        try {
            doCreate(factory);
        } catch (DuplicateKeyException ex) {
            throw new ConflictException(ex.getLocalizedMessage());
        } catch (IntegrityConstraintViolationException ex) {
            throw new ConflictException("Could not create factory with creator that refers on non-existent user");
        } catch (RuntimeException ex) {
            throw new ServerException(ex.getLocalizedMessage(), ex);
        }
        return new FactoryImpl(factory);
    }

    @Override
    public FactoryImpl update(FactoryImpl update) throws NotFoundException, ConflictException, ServerException {
        requireNonNull(update);
        try {
            return new FactoryImpl(doUpdate(update));
        } catch (DuplicateKeyException ex) {
            throw new ConflictException(ex.getLocalizedMessage());
        } catch (RuntimeException ex) {
            throw new ServerException(ex.getLocalizedMessage(), ex);
        }
    }

    @Override
    public void remove(String id) throws ServerException {
        requireNonNull(id);
        try {
            doRemove(id);
        } catch (RuntimeException ex) {
            throw new ServerException(ex.getLocalizedMessage(), ex);
        }
    }

    @Override
    @Transactional
    public FactoryImpl getById(String id) throws NotFoundException, ServerException {
        requireNonNull(id);
        try {
            final FactoryImpl factory = managerProvider.get().find(FactoryImpl.class, id);
            if (factory == null) {
                throw new NotFoundException(format("Factory with id '%s' doesn't exist", id));
            }
            return new FactoryImpl(factory);
        } catch (RuntimeException ex) {
            throw new ServerException(ex.getLocalizedMessage(), ex);
        }
    }

    @Override
    @Transactional
    public List<FactoryImpl> getByAttribute(int maxItems,
                                            int skipCount,
                                            List<Pair<String, String>> attributes) throws ServerException {
        try {
            LOG.info("FactoryDao#getByAttributes #maxItems: {} #skipCount: {}, #attributes: {}", maxItems, skipCount, attributes);
            final Map<String, String> params = new HashMap<>();
            String query = "SELECT factory FROM Factory factory";
            if (!attributes.isEmpty()) {
                final StringJoiner matcher = new StringJoiner(" AND ", " WHERE ", " ");
                int i = 0;
                for (Pair<String, String> attribute : attributes) {
                    final String parameterName = "parameterName" + i++;
                    params.put(parameterName, attribute.second);
                    matcher.add("factory." + attribute.first + " = :" + parameterName);
                }
                query = query + matcher;
            }
            final TypedQuery<FactoryImpl> typedQuery = managerProvider.get()
                                                                      .createQuery(query, FactoryImpl.class)
                                                                      .setFirstResult(skipCount)
                                                                      .setMaxResults(maxItems);
            for (Map.Entry<String, String> entry : params.entrySet()) {
                typedQuery.setParameter(entry.getKey(), entry.getValue());
            }
            return typedQuery.getResultList()
                             .stream()
                             .map(FactoryImpl::new)
                             .collect(Collectors.toList());
        } catch (RuntimeException ex) {
            throw new ServerException(ex.getLocalizedMessage(), ex);
        }
    }

    @Transactional
    protected void doCreate(FactoryImpl factory) {
        final EntityManager manager = managerProvider.get();
        if (factory.getWorkspace() != null) {
            factory.getWorkspace().getProjects().forEach(ProjectConfigImpl::prePersistAttributes);
        }
        manager.persist(factory);
        manager.flush();
    }

    @Transactional
    protected FactoryImpl doUpdate(FactoryImpl update) throws NotFoundException {
        final EntityManager manager = managerProvider.get();
        if (manager.find(FactoryImpl.class, update.getId()) == null) {
            throw new NotFoundException(format("Could not update factory with id %s because it doesn't exist", update.getId()));
        }
        if (update.getWorkspace() != null) {
            update.getWorkspace().getProjects().forEach(ProjectConfigImpl::prePersistAttributes);
        }
        FactoryImpl merged = manager.merge(update);
        manager.flush();
        return merged;
    }

    @Transactional
    protected void doRemove(String id) {
        final EntityManager manager = managerProvider.get();
        final FactoryImpl factory = manager.find(FactoryImpl.class, id);
        if (factory != null) {
            manager.remove(factory);
            manager.flush();
        }
    }

    @Singleton
    public static class RemoveFactoriesBeforeUserRemovedEventSubscriber
            extends CascadeEventSubscriber<BeforeUserRemovedEvent> {
        @Inject
        private FactoryDao   factoryDao;
        @Inject
        private EventService eventService;

        @PostConstruct
        public void subscribe() {
            eventService.subscribe(this, BeforeUserRemovedEvent.class);
        }

        @PreDestroy
        public void unsubscribe() {
            eventService.unsubscribe(this, BeforeUserRemovedEvent.class);
        }

        @Override
        public void onCascadeEvent(BeforeUserRemovedEvent event) throws ServerException {
            final Pair<String, String> factoryCreator = Pair.of("creator.userId", event.getUser().getId());
            for (FactoryImpl factory : factoryDao.getByAttribute(0, 0, singletonList(factoryCreator))) {
                factoryDao.remove(factory.getId());
            }
        }
    }
}
